/* CS32 FINAL PROJECT */

package GameEngine;

import ClientGraphics.GraphicsMain;
import GameEngine.GameConstants.Graphic;
import GameEngine.GameConstants.Levels;
import GameEngine.GameConstants.Special;
import Graphics.MainGui;
import Objects.BasicObject;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * SERVER/CLIENT PROTOCOL:
 * 
 * Upon Connection, Server waits for Client to verify they are ready to play by listening for a byte.
 *      Client: sends special (byte)
 *      Server: sends ID (short)
 *      Server: sends level background reference (byte)
 *      Server: sends level width (int)
 *      Server: sends level height (int)
 * When all Clients are ready to play the server sends a ready indication to all connected clients
 *      Server: sends game beginning (boolean = true)
 * Once the game begins, the client continuously reads for the state of the game from the server.
 *      Server: sends the initial state of all objects in level to all connected clients
 *          ****** LOOP *******
 *          idnum       : short
 *          graphic     : byte
 *          X direction : float
 *          Y direction : float
 *          X center    : short
 *          Y center    : short
 *          X size      : short
 *          Y Size      : short
 *          exists?     : boolean
 *      Server: sends end of objects (boolean = true)
 *      Client: continuously reads this data above
 * At this point, the server sends at timed intervals the state of the game with the protocol above.
 * Meanwhile, the IncomingClientHandler is continuously reading for actions from the associated client.
 * When an action is received, the information is passed into the calculation for the new state of the level
 *      Client: able to send actions (byte)
 *      Server: continuously reads for actions (bytes)
 * When level calculates a victory condition, the server sends a dummy object with ID = -7. 
 * When client reads this, they set variable that prevents them from sending anymore actions to the server
 * Client then responds acknowledging they know level is over.
 *      Server: sends dummy object (short = -7)
 *      Client: sends acknowledgment (byte = -7)
 * Currently when this happens the server closes and all users are sent back to the main menu.  If work continues on this project
 * we will alter the events following victory condition.
 */

 /**
 * This class handles IO between game player and server
 * 
 * @author crdunwel
 */
public class Client
{
    // INSTANCE VARIABLES
    private Special type;                   // the special power for the player (found in GameConstants under Special enum)
    public GameServer inGame;               // reference to the server
    public String addrname;                 // the address for the server
    public int port;                        // the port for the server
    public Socket socket;                   // client's socket to connect to the server
    public String username;                 // username for client
    public ClientThread userThread;         // thread that reads information from server to render graphically on screen
    public DataOutputStream out;            // stream to write action to server
    public boolean serverReady;             // boolean to indicate that the server has started the game and client should begin reading objects to render
    public MainGui guiReference;            // used to return to GUI after a victory
    public JFrame mainFrame;                // used for testing purposes
    public JPanel gamePanel;                // panel that graphics are drawn to

    /**
     * CONSTRUCTOR
     * User
     *
     * This class handles IO with the server by writing user actions to a bytestream.
     * Once an action is written, the server must update the state of the level and
     * send the updated state back to the User.
     *
     * @param name      the username for the player
     * @param port      the port the player is connected to
     * @param addrname  the address name for the player to connect to
     * @param s         the special the player decides
     */
    public Client(JFrame frame, String name, int p, MainGui guiMain)
    {
       mainFrame = frame;
       guiReference = guiMain;
       addrname = name;
       port = p;
       socket = createSocket(addrname, port);
       if (socket != null)
       {
            try
            {
                out = new DataOutputStream(socket.getOutputStream());
            }
            catch (IOException e1)
            {
                System.out.println("failed to create dataoutputstream");
            }
       }
    }
    
    /**
     * This method takes an address and a port and outputs a Socket to connect to this address/port.
     *
     * @param addrname      the address name for the player to connect to
     * @param port          the port for the player to connect to
     * @return Socket       the Socket connected to addrname on port
     */
    public Socket createSocket(String addrname, int port)
    {
        try
        {
            InetAddress addr = InetAddress.getByName(addrname);
            Socket sock = new Socket(addr, port);
            return sock;
        }
        catch (IOException e1)
        {
            return null;        
        }
    }

    /**
     * This method begins the user's read thread.  It is called after the user pressed the READY button
     */
    public void initializeUserThread()
    {
        userThread = new ClientThread();
        sendUserType();
        userThread.start();
    }

    public void sendUserType()
    {
        try
        {
            out.writeByte((byte) type.ordinal());
            System.out.println("Client: type = " + type.ordinal());
        }
        catch (IOException e1)
        {
            System.out.println("error writing byte: " + type.ordinal());
        }
    }

    /**
     * This method allows the player to send a message of their actions to the server
     * 
     * @param act 
     */
    public void writeAction(GameConstants.Action act)
    {
        try
        {
            if (serverReady)
            {
                out.writeByte((byte) act.ordinal());
            }
        }
        catch (IOException e2)
        {

        }
    }

    /**
     * This method sends the server an acknowledgment that the client knows the game is over
     */
    public void victoryResponse()
    {
        try
        {
            out.writeByte(-7);
            socket.close();
        }
        catch (IOException e1)
        {
            System.out.println("Client Error: could not send acknowledgment that the game is over");
        }
    }
    
    //************GETTERS AND SETTERS**********************//
    public void setUserName(String name)
    {
        username = name;
    }

    public void setType(Special spec)
    {
        type = spec;
    }

    public Socket getSocket()
    {
        return socket;
    }

    public void reconnect()
    {
        socket = createSocket(addrname, port);
    }
    
    public JPanel getPanel()
    {
        return gamePanel;
    }
    

    /**
     * This private class handles IO with the Server via a thread.
     */
    private class ClientThread extends Thread
    {
        // INSTANCE VARIABLES
        private DataInputStream in;                      // handles input from the server
        public HashMap<Short, BasicObject> objects;      // keeps track of objects to render to client's screen
        public Levels levelbg;                           // the background for the level
        public int levelSizeX;                           // level width
        public int levelSizeY;                           // level height
        public GraphicsMain gMain;                       // reference to graphic renderer
        public short UID;                                // the UID the server assigns to the client
     
        /**
         * CONSTRUCTOR
         * UserThread
         *
         * The UserThread continuously reads for input from server
         */
        public ClientThread()
         {
            try
            {
                in = new DataInputStream(socket.getInputStream());
                objects = new HashMap<Short, BasicObject>();
            }
            catch (IOException e1)
            {
                System.out.println("Client Error: could not construct ClientThread");
            }
         }       
               
         /**
          * RUN METHID
          *  
          * This is the run method for Client
          */
         @Override
         public void run()
         {
            try
            {
                // receives initial information from server
                receiveInitialGameInfo();        
                // initializes graphics components
                initGraphicsEngine();
                // check if server is ready to begin game
                boolean serverStatus = in.readBoolean();
                serverReady = serverStatus;
                // begins to display graphics sent to user from server
                gMain.initialize();
                while (serverReady)
                {
                    // read objects sent by server
                    readAndSetObjects();
                }
                // sends server verification that client knows levels is over
                // method is contained in parent class
                victoryResponse();
                // returns client back to main screen of GUI
                returnMainMenu();          
            }
            catch (IOException ex)
            {
                System.out.println("Client Error: could not read from server");
            }
         }
         
        /**
         * This method informs the client about necessary variables pertaining to the level and their player's object ID in that level.
         */
        public void receiveInitialGameInfo()
        {
            try
            {
                // reads UID assigned to client by server
                UID = in.readShort();
                // reads level background from the server
                byte levelbgByte = in.readByte();
                // converts the byte to a Levels enum
                levelbg = GameConstants.parseByteToLevels(levelbgByte);
                // reads level size from server
                levelSizeX = in.readInt();
                levelSizeY = in.readInt();
            }
            catch (IOException e1)
            {
                System.out.println("Client Error: failed to receive level info");
            }
        }
        
        /**
         * This method initializes the graphics engine and displays the wait screen to the client
         */
        public void initGraphicsEngine()
        {
            // sets up GUI to display graphics to user
            gMain = new GraphicsMain(levelbg, new Dimension(mainFrame.getWidth(), mainFrame.getHeight()), levelSizeX, levelSizeY);
            // makes it so you follow your sprite on the screen
            gMain.setUID(UID);    
            // to allow for system testing without gui
            if (guiReference == null)
            {
                gamePanel = new JPanel();
            }
            // otherwise normal mode
            else 
            {
                gamePanel = guiReference.getGamePanel();
            }
            gMain.setGraphicsPanel(gamePanel);
            gMain.drawWaitScreen();
        }
        
        /**
         * This method reads the objects from the server, constructs a BasicObject and adds it to a HashMap that keeps track of what objects
         * are currently on the screen and where their positions are.
         */
        public void readAndSetObjects()
        {
           try
           {
               while (true)
               {
                   short idnum = in.readShort();
                   // victory condition
                   if (idnum == -7)
                   {
                       serverReady = false;
                       break;
                   }
                   else
                   {                       
                        // READ OBJECT DATA PRIMTIVE
                        byte graph = in.readByte();
                        float dirX = in.readFloat();
                        float dirY = in.readFloat();
                        short cenX = in.readShort();
                        short cenY = in.readShort();
                        short sizeX = in.readShort();
                        short sizeY = in.readShort();
                        boolean exists = in.readBoolean();
                        
                        // GET PRIMITIVE DATA TO BASICOBJECT COMPONENTS
                        Vector center = new Vector(cenX,cenY);
                        Vector direction = new Vector(dirX,dirY);
                        Vector size = new Vector(sizeX,sizeY);
                        Graphic graphic = GameConstants.parseByteToGraphic(graph);
                        
                        // checks if object already exists for client and if so updates values
                        if (objects.containsKey(idnum))
                        {
                            objects.get(idnum).setCenter(center);
                            objects.get(idnum).setDir(direction);
                            objects.get(idnum).setSize(size);
                            objects.get(idnum).setGraph(graphic);
                            objects.get(idnum).setExists(exists);
                        }
                        // creates and adds objects to map if it doesn't exist
                        else
                        {
                            BasicObject toAdd = new BasicObject(direction,center,size,graphic);
                            toAdd.setId(idnum);
                            toAdd.setExists(exists);
                            objects.put(idnum, toAdd);
                        }
                        // checks if end of object list
                        if (in.readBoolean() == true)
                        {
                            gMain.update(HashMapToList());
                            break;
                        }
                   }
               }
           }
           catch (IOException e1)
           {
                System.out.println("Client Error: could not read BasicObject primitive data");
           }
        }

        /**
         * This method converts the BasicObjects in a hashmap to a list of BasicObjects
         * 
         * @return  list form of BasicObjects
         */
        public List<BasicObject> HashMapToList()
        {
            Set<Short> keys = objects.keySet();
            List<BasicObject> toReturn = new LinkedList<BasicObject>();
            for (Short id : keys)
            {
                toReturn.add(objects.get(id));
            }
            return toReturn;
        }
        
        /**
         * This method returns user back to main menu once victory occurs.
         */
        public void returnMainMenu()
        {
             gMain.stopRendering();
             mainFrame.remove(gMain.getGraphicsPanel());
             CardLayout cl = (CardLayout)(guiReference.getContainPanel().getLayout());
             cl.show(guiReference.getContainPanel(), "Main");
        }
    }
}
